Ограничения TypeScript

Читать на англ.

17.02.2024 | 3-4 мин. чтения

Или как я пытался написать перегрузку методов в TS не без помощи ChatGPT. Ссылка на обмен сообщениями с ChatGPT

Когда я начал изучать такую вещь, как перегрузка методов, у меня была идея реализовать это как-то в typescipt. Моя цель была ввести удобную для использования перегрузку, не делая слишком много ручной обработки, как было в примерах при подготовке, изучении области.

Вся идея заключалась в том, чтобы сделать что-то подобное, что есть в Java для TS, учитывая, что TS "поддерживает" перегрузку методов только на этапе компиляции, имея одноименные методы в классе, но с разными сигнатурами и один метод, так называемый, входная точка или входной метод. Также это означает, что программист должен проводить проверку типов сам в первую очередь:

class TestClass {
    someMethod(obj: TypeOne): void; // метод с "перегруженной" сигнатурой
    someMethod(obj: TypeTwo): void; // метод с "перегруженной" сигнатурой
    someMethod(obj: TypeOneOrTwo): void { // входной метод
        // важно проверить тип объекта на соответствие нашим условиям
        if (obj && _check_obj_type_matching_somehow_)) {
            // выполнять какое-то действие 
        }
        else { // может быть еще одно условие  `else if` для проверки типа
            // выполнять какое-то другое действие
        }
    }
};

Таким образом, проблема состояла в том, чтобы как-то проверить, что предоставленный объект имеет один из подтипов объединенного типа, и здесь я столкнулся с основной проблемой. В JS/TS есть EСMAScript Data Types and Values, сверяясь с которыми, мы можем проверить действительный тип чего-либо, например, используя ключевое слово typeof.

Но общая цель заключается в проверке типа входного объекта в методе с перегрузкой в сравнении с пользовательски определенными типами. Мы можем добиться этого несколькими способами. Первый - вручную проверять наличие свойств входного объекта в конструкции type guard:

type TypeOne = {
    name: string;
    id: number;
};

function isTypeOne(obj: any): obj is TypeOne {
    return (
        'name' in obj && 'id' in obj &&
        typeof (obj as TypeOne).name === 'string' &&
        typeof (obj as TypeOne).id === 'number'
    );
}

И это включает в себя худший сценарий работы с проверкой большого количества полей большого объекта, сопоставляя их типы явно, но вручную. Это может быть сделано в автоматизированном виде, проверяющем свойства с использованием Object.keys и представляющем его через собранные типы значений из подписи объекта - но это действительно громоздко. Также мы можем использовать оператор satisfies TS версии 4.9 для проверки типа, но он не работает так, как кажется:

type TypeOne = {
    name: string;
    id: number;
};

var obj = { id: 1, name: 2};

if (obj satisfies TypeOne) {
  console.log(1) // 1  будет выведено в лог
} else {
  console.log(2)
}

Потому что "Новый оператор satisfies позволяет нам проверить, что тип выражения соответствует некоторому типу, не изменяя получившийся тип этого выражения." (цитата). Так что он просто покажет вам сообщение об ошибке типа на этапе компиляции в IDE, но код все равно будет выполняться без ошибок.

После некоторого времени я понял, что подходящим решением может быть только то, что соответствует этим двум критериям:

  • Проверка типов должна быть динамической, что означает способность использовать универсальные шаблоны и иметь возможность проверять все типы одним действием.
  • Если производится проверка наличия свойств и их типов данных, она должна быть автоматической

Борясь с ошибками и лазая по сети, играя с кодом, я не смог найти ответов, которые бы удовлетворяли моим целям. Главным фактором было то, что использование union типов и проверка его подтипа ни в форме универсального шаблона, ни прямо не работают и всегда вызывают ошибку:

type TypeOne = {
    name: string;
    id: number;
};
type TypeTwo = 
type TypeThree = 

type TMerged =
    | TypeOne
    | TypeTwo
    | TypeThree

// Создаем объект map , который регистрирует каждую функцию проверки типа
const typeCheckMap: TypeCheckMap = {
    'TypeOne': isTypeOne,
    'TypeTwo': isTypeTwo,
    // Добавьте другие типы по мере необходимости
};

// Функция проверки типа с универсальными терминами
function isType<T>(obj: TMerged, typeName: keyof typeof typeCheckMap): obj is T {
    const typeChecker = typeCheckMap[typeName];
    if (!typeChecker) {
        throw new Error(`Type check for '${typeName}' not found.`);
    }
    return typeChecker(obj);
}

// ERROR:
// A type predicate's type must be assignable to its parameter's type.
//  Type 'T' is not assignable to type 'TMerged'.
//    Type 'T' is not assignable to type 'TypeTwo'

Причина в том, что поля типов, требуемые в TypeOne, но отсутствующие в TypeTwo и т. д., противоречат друг другу. TypeScript не может обработать тот факт, что предикат типа может быть назначаемым и заменяющим супер-тип TMerged.

Здесь и появился ChatGPT. После ряда вопросов я наконец-то понял проблему - это невозможность TS проверки типов данных во время ран-тайма, а также структурный принцип проверки типов structural, поэтому нет совместимости между внутренними типами union типа, он влияет на соответствие напрямую между супер-типом и его подтипом, т.е. тип TMerged не назначается типу TypeTwo и т. д. из-за их структурных различий. Даже расширение второго первым не решает проблему.

Так что окончательный вывод - единственный способ сегодня использовать перегрузку метода в TS - все еще полу-ручная проверка типов.